[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータを使用してオブジェクト検出でモデルを作成してみました
1 はじめに
CX事業本部の平内(SIN)です。
Amazon SageMaker の組み込みの物体検出アルゴリズム(object-detection)では、Amazon SageMaker Ground Truth(以下、Ground Truth)で作成したデータを使用することができます。
作業としては、Ground Truthで生成されたoutput.manifestを、学習用(train)と検証用(validation)に分割するだけです。
作成されたデータ内のラベルが、ある程度均一に、そして大量にある場合は、気にすることでは無いかも知れないでしょうが、今回は、ラベル数が少数で、単純に分割できない場合も考慮して、分割してみました。
2 Ground Truth
Ground Truthのデータは、下記で使用したものです。
分割の結果を確認しやすいように、あえて非常に少数のデータとなっています。
- 画像数 50枚
- ラベル(AHIRU×50 (DOG)×10
アウトプットは、s3://sagemaker-working-bucket-001/GroundTruth-output/AHIRU-Project/manifests/output/output.manifestになっています。
3 学習及び、検証データ
学習用及び、検証用に分割しているのは、以下のコード(Jupyter Notebook)です。
対象となる全てのデータから、含まれるラベルの数をカウントし、サンプル数の少ないラベルから順に、分割しています。
bucket_name = "sagemaker-working-bucket-001" prefix = 'object-detection-with-ground-truth'
# Ground Truthの出力(output.manifest)をダウンロードする inputManifestPath = 's3://sagemaker-working-bucket-001/GroundTruth-output/AHIRU-Project/manifests/output/output.manifest' !aws s3 cp $inputManifestPath "./output.manifest"
# output.manifestをtrain及びvalidationに分割する import json # 1件のデータの表現するクラス(定義されているラベルを把握するために使用する) class Data(): def __init__(self, src): self.src = src # プロジェクト名の取得 for key in src.keys(): index = key.rfind("-metadata") if(index!=-1): self.projectName = key[0:index] cls_map = src[self.projectName + "-metadata"]["class-map"] # アノテーション一覧からクラスIDの指定を取得する self.annotations = [] for annotation in src[self.projectName]["annotations"]: id = annotation['class_id'] self.annotations.append({ "label":cls_map[str(id)] }) # 指定されたラベルを含むかどうか def exsists(self, label): for annotation in self.annotations: if(annotation["label"] == label): return True return False # 全てのJSONデータを読み込む def getDataList(inputPath): dataList = [] with open(inputPath, 'r') as f: srcList = f.read().split('\n') for src in srcList: if(src != ''): dataList.append(Data(json.loads(src))) return dataList # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) def getLabel(dataList): labels = {} for data in dataList: for annotation in data.annotations: label = annotation["label"] if(label in labels): labels[label] += 1 else: labels[label] = 1 # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) labels = sorted(labels.items(), key=lambda x:x[1]) return labels # dataListをラベルを含むものと、含まないものに分割する def deviedDataList(dataList, label): targetList = [] unTargetList = [] for data in dataList: if(data.exsists(label)): targetList.append(data) else: unTargetList.append(data) return (targetList, unTargetList) # 学習用と検証用の分割比率 ratio = 0.8 # 80%対、20%に分割する # GroundTruthの出力 inputManifestFile = './output.manifest' # SageMaker用の出力 outputTrainFile = './train' outputValidationFile = './validation' dataList = getDataList(inputManifestFile) projectName = dataList[0].projectName print("全データ: {}件 ".format(len(dataList))) # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる) labels = getLabel(dataList) for i,label in enumerate(labels): print("[{}]{}: {}件 ".format(i, label[0], label[1])) # 保存済みリスト storedList = [] # 学習及び検証用の出力 train = '' validation = '' # ラベルの数の少ないものから優先して分割する for i,label in enumerate(labels): print("{} => ".format(label[0])) # dataListをラベルが含まれるものと、含まないものに分割する (targetList, unTargetList) = deviedDataList(dataList, label[0]) # 保存済みリストから、当該ラベルで既に保存済の件数をカウントする (include, notInclude) = deviedDataList(storedList, label[0]) storedCounst = len(include) # train用に必要な件数 count = int(label[1] * ratio) - storedCounst print("train :{}".format(count)) # train側への保存 for i in range(count): data = targetList.pop() train += json.dumps(data.src) + '\n' storedList.append(data) # validation側への保存 print("validation :{} ".format(len(targetList))) for data in targetList: validation += json.dumps(data.src) + '\n' storedList.append(data) dataList = unTargetList print("残り:{}件".format(len(dataList))) with open(outputTrainFile, mode='w') as f: f.write(train) with open(outputValidationFile, mode='w') as f: f.write(validation) num_training_samples = len(train.split('\n')) print("projectName:{} num_training_samples:{}".format(projectName, num_training_samples))
コードを実行すると、下記の出力が得られます。
ラベルAHIRUとDOGは、それぞれ、30:10 8:2件に分けられています。 また、TraningJob作成時にパラメータとして使用される、プロジェクト名(AHIRU_Project)及び、学習データの件数(num_traning_samples:39)も、output.manifestから読み取られています。
4 S3へのアップロード
分割して作成された、2つのファイル(trainとvalidation)は、SageMakerから利用可能なように、S3にアップロードします。
# 分割した train及び、validationをS3にアップロードする s3_train = "s3://{}/{}/train".format(bucket_name, prefix) !aws s3 cp $outputTrainFile $s3_train s3_validation = "s3://{}/{}/validation".format(bucket_name, prefix) !aws s3 cp $outputValidationFile $s3_validation
5 Traning
最初に、学習用のコンテナを準備します。
import boto3 import sagemaker from sagemaker import get_execution_role role = get_execution_role() sess = sagemaker.Session() # 学習用のコンテナ取得 training_image = sagemaker.amazon.amazon_estimator.get_image_uri(boto3.Session().region_name, 'object-detection', repo_version='latest') # モデルを出力するバケットの指定 s3_output_path = "s3://{}/{}/output".format(bucket_name, prefix)
training_jobに設定するパラメータは、以下のとおりです。
作成した、train及び、validationは、InputDataConfigで3DataType:AugmentedManifestFileとして渡されています。
epochsが、200と多いですが、データがごく少数なので、学習は、数分で完了します。
# パラメータの作成 import time from time import gmtime, strftime job_name_prefix = 'groundtruth-to-sagemaker' timestamp = time.strftime('-%Y-%m-%d-%H-%M-%S', time.gmtime()) job_name = job_name_prefix + timestamp training_params = { "AlgorithmSpecification": { "TrainingImage": training_image, "TrainingInputMode": "Pipe" }, "RoleArn": role, "OutputDataConfig": { "S3OutputPath": s3_output_path }, "ResourceConfig": { "InstanceCount": 1, "InstanceType": "ml.p3.2xlarge", "VolumeSizeInGB": 50 }, "TrainingJobName": job_name, "HyperParameters": { "base_network": "resnet-50", "use_pretrained_model": "1", "num_classes": "2", "mini_batch_size": "10", "epochs": "200", "learning_rate": "0.001", "lr_scheduler_step": "3,6", "lr_scheduler_factor": "0.1", "optimizer": "rmsprop", "momentum": "0.9", "weight_decay": "0.0005", "overlap_threshold": "0.5", "nms_threshold": "0.45", "image_shape": "300", "label_width": "350", "num_training_samples": str(num_training_samples) }, "StoppingCondition": { "MaxRuntimeInSeconds": 86400 }, "InputDataConfig": [ { "ChannelName": "train", "DataSource": { "S3DataSource": { "S3DataType": "AugmentedManifestFile", "S3Uri": s3_train, "S3DataDistributionType": "FullyReplicated", "AttributeNames": ["source-ref",projectName] } }, "ContentType": "application/x-recordio", "RecordWrapperType": "RecordIO", "CompressionType": "None" }, { "ChannelName": "validation", "DataSource": { "S3DataSource": { "S3DataType": "AugmentedManifestFile", "S3Uri": s3_validation, "S3DataDistributionType": "FullyReplicated", "AttributeNames": ["source-ref",projectName] } }, "ContentType": "application/x-recordio", "RecordWrapperType": "RecordIO", "CompressionType": "None" } ] } print('Training job name: {}'.format(job_name)) print('\nInput Data Location: {}'.format(training_params['InputDataConfig'][0]['DataSource']['S3DataSource']))
学習の開始は、以下です。
# Traning client = boto3.client(service_name='sagemaker') client.create_training_job(**training_params)
ジョブの状態が、Training job current status: Completedとなれは、学習は完了です。
# 学習ジョブの状態取得 status = client.describe_training_job(TrainingJobName=job_name)['TrainingJobStatus'] print('Training job current status: {}'.format(status))
トレーニングの状態は、AWSコンソールからも確認可能です。
ログは、CloudWatch Logsで確認でき、最終的に、mAP score は、0.51となていました。
quality_metric: host=algo-1, epoch=199, validation mAP =(0.5106382978723404) Updating the best model with validation-mAP=0.5106382978723404
6 最後に
今回は、Ground Truthで作成したデータを使用して、SageMakerのビルトイン(オブジェクト検出)を使用してみました。特に変換などは必要無いので、サンプル数が潤沢であれば、下記のように単純に分割するだけでも大丈夫でしょう。
with jsonlines.open('output.manifest','r') as reader: lines = list(reader) np.random.shuffle(lines) # ランダム化 # 全データ件数 dataset_size = len(lines) # 学習用の件数 num_traning_samples = round(dataset_size*0.8) # 分割 train_data = lines[:num_traning_sample] validation_data = [lines[num_traning_samples:]
ごく少数のサンプルで不十分ですが、一応、動作していることを確認できました。
使用したNotebookは、下記に置きました。